/* * MailQueue.java * * This work is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation; either version 2 of the License, * or (at your option) any later version. * * This work is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA * * Copyright (c) 2004-2006 Per Cederberg. All rights reserved. */ package org.liquidsite.util.mail; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.Properties; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.mail.AuthenticationFailedException; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.NoSuchProviderException; import javax.mail.SendFailedException; import javax.mail.Session; import javax.mail.Transport; import org.liquidsite.util.log.Log; /** * An outgoing email queue. This class is used for storing outgoing * email messages for before being sent. Normally this queue should be * empty, as queueing is performed in the receiving SMTP server. This * queue is only used to reduce the reponse latency when processing * web requests. The actual sending of the mails should be performed * by a background thread. * * @author Per Cederberg, <per at percederberg dot net> * @version 1.0 */ public class MailQueue { /** * The class logger. */ private static final Log LOG = new Log(MailQueue.class); /** * The maximum mail process queue size. */ private static final int MAX_PROCESS_SIZE = 5; /** * The maximum mail wait queue size. */ private static final int MAX_WAIT_SIZE = 1000; /** * The maximum number of email messages to send from each * processing round of a message. */ private static final int MAX_SEND_COUNT = 10; /** * The default mail message header. */ private static final String DEFAULT_HEADER = ""; /** * The default mail message footer. */ private static final String DEFAULT_FOOTER = "\n\n" + "-----------------------------------------------------------------\n" + "Message created and sent through Liquid Site (www.liquidsite.net)\n" + "If you are not the intended recipient of this message, please\n" + "forward it to abuse@liquidsite.net for investigation. Be sure to\n" + "include the whole message, including the following lines.\n"; /** * The email address regular expression. */ private static final Pattern ADDRESS_RE = Pattern.compile("<.+>"); /** * The one and only mail queue instance. */ private static MailQueue instance = null; /** * The mail processing queue. This queue is completely internal * and is only modified when processing mails (and there is * therefore no need for thread synchronization). The mail * messages in this queue are processed in a round-robin fashion * until all mails have been generated. At that point the mail * message is removed from this queue. */ private ArrayList processQueue = new ArrayList(MAX_PROCESS_SIZE); /** * The mail wait queue. New messages are added last to this queue * by any thread in the system. All accesses to this queue must * therefore be strictly synchronized to avoid race conditions. * The mail messages are moved from this queue to the process * queue in FIFO order when a processing slot is available. */ private LinkedList waitQueue = new LinkedList(); /** * The mail session. This object contains the SMTP configuration * to use. */ private Session session = null; /** * The header to automatically add to all messages. If set to an * empty string, no header will be added. If set to null, a * default header will be added. */ private String header = null; /** * The footer to automatically add to all messages. If set to an * empty string, no footer will be added. If set to null, a * default footer will be added. */ private String footer = null; /** * Returns the mail queue instance. * * @return the mail queue instance */ public static MailQueue getInstance() { if (instance == null) { instance = new MailQueue(); } return instance; } /** * Creates a new mail queue. This constructor should only be * called once to avoid duplicate queues. */ private MailQueue() { // No further initialization needed } /** * Initializes this mail queue. * * @param host the mail host, or null for "localhost" * @param user the mail user, or null for none * @param from the mail from address, or null for none */ public void initialize(String host, String user, String from) { Properties props = new Properties(); Matcher m; props.setProperty("mail.transport.protocol", "smtp"); if (host == null) { host = "localhost"; } props.setProperty("mail.host", host); if (user != null) { props.setProperty("mail.user", user); } if (from != null) { props.setProperty("mail.from", from); m = ADDRESS_RE.matcher(from); from = m.find() ? m.group() : "<" + from + ">"; props.setProperty("mail.smtp.from", from); } props.setProperty("mail.smtp.connectiontimeout", "60000"); props.setProperty("mail.smtp.timeout", "60000"); session = Session.getInstance(props); } /** * Returns the current mail header. * * @return the current mail header */ public String getHeader() { if (header == null) { return DEFAULT_HEADER; } else { return header; } } /** * Sets the mail header. * * @param header the new header, or null for default */ public void setHeader(String header) { this.header = header; } /** * Returns the current mail footer. * * @return the current mail footer */ public String getFooter() { if (footer == null) { return DEFAULT_FOOTER; } else { return footer; } } /** * Sets the mail footer. * * @param footer the new footer, or null for default */ public void setFooter(String footer) { this.footer = footer; } /** * Adds a new message to the queue. This method is thread-safe. * * @param message the message to add * * @throws MailMessageException if the message wasn't valid or if * the queue was full */ public void add(MailMessage message) throws MailMessageException { String error; if (!message.isValid()) { error = "invalid mail message to '" + message.getRecipient() + "'"; LOG.warning(error); throw new MailMessageException(error); } if (waitQueue.size() >= MAX_WAIT_SIZE) { error = "mail queue full, message to '" + message.getRecipient() + "' rejected"; LOG.error(error); throw new MailMessageException(error); } if (session == null) { error = "mail not initialized, message to '" + message.getRecipient() + "' rejected"; LOG.error(error); throw new MailMessageException(error); } adjustMessageText(message); enqueue(message); LOG.trace("queued mail message to '" + message.getRecipient() + "'"); } /** * Adjust the message text by adding the configured message * header and footer. * * @param message the message to modify */ private void adjustMessageText(MailMessage message) { StringBuffer buffer = new StringBuffer(); Iterator iter; String str; buffer.append(getHeader()); buffer.append(message.getText()); buffer.append(getFooter()); buffer.append("\n"); iter = message.getAttributeNames().iterator(); while (iter.hasNext()) { str = iter.next().toString(); buffer.append(str); buffer.append(": "); buffer.append(message.getAttribute(str)); buffer.append("\n"); } message.setText(buffer.toString()); } /** * Processes the mail messages in the queue. If the queue is * empty, nothing is done. The messages are removed from the * queue only if sent correctly or if they are invalid. On mail * transport error, the mail messages will remain in the queue. * * @throws MailTransportException if the mail transport couldn't * be initialized correctly */ public void process() throws MailTransportException { MailMessage message; // Fill up process queue while (processQueue.size() < MAX_PROCESS_SIZE && waitQueue.size() > 0) { message = dequeue(); processQueue.add(message); LOG.trace("starting processing of mail message to '" + message.getRecipient() + "'"); } // Loop through process queue once for (int i = 0; i < processQueue.size(); i++) { message = (MailMessage) processQueue.get(i); try { processMessage(message); } catch (MailMessageException e) { // Do nothing, message will skip to next address } if (!message.hasMoreMessages()) { processQueue.remove(i--); LOG.trace("finished processing of mail message to '" + message.getRecipient() + "'"); } } } /** * Processes the specified mail message. This will attempt to * send a number of generated mails from the message, although * not all. * * @param message the mail message to send * * @throws MailTransportException if the mail transport couldn't * be initialized correctly * @throws MailMessageException if the mail message couldn't be * sent due to an error in the message */ private void processMessage(MailMessage message) throws MailTransportException, MailMessageException { Transport transport; Message msg; String error; int count = 0; // Connect to SMTP server try { transport = session.getTransport(); } catch (NoSuchProviderException e) { error = "failed to create SMTP transport"; LOG.error(error, e); throw new MailTransportException(error, e); } try { transport.connect(); } catch (AuthenticationFailedException e) { error = "failed to authenticate to SMTP server"; LOG.error(error, e); throw new MailTransportException(error, e); } catch (MessagingException e) { error = "unknown error while sending message"; LOG.error(error, e); throw new MailTransportException(error, e); } catch (IllegalStateException e) { error = "already connected to SMTP transport"; LOG.error(error, e); throw new MailTransportException(error, e); } // Send a number of mail messages try { while (message.hasMoreMessages() && count < MAX_SEND_COUNT) { msg = message.getNextMessage(session); transport.sendMessage(msg, msg.getAllRecipients()); count++; } } catch (SendFailedException e) { error = "failed to send mail message"; LOG.error(error, e); throw new MailMessageException(error, e); } catch (MessagingException e) { error = "unknown error while sending message"; LOG.error(error, e); throw new MailMessageException(error, e); } finally { try { transport.close(); } catch (MessagingException ignore) { // Ignore this } } } /** * Adds a new message last in the wait queue. This method is * thread-safe. * * @param message the mail message to add */ private synchronized void enqueue(MailMessage message) { waitQueue.addLast(message); } /** * Removes the first message in the wait queue and returns it. * This method is thread-safe. * * @return the dequeued mail message, or * null if the queue was empty */ private synchronized MailMessage dequeue() { if (waitQueue.isEmpty()) { return null; } else { return (MailMessage) waitQueue.removeFirst(); } } }